/*! ******************************************************************************
 *
 * Pentaho
 *
 * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file.
 *
 * Change Date: 2029-07-20
 ******************************************************************************/


package org.pentaho.platform.repository2.unified.jcr;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.api.security.user.Group;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAce;
import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl;
import org.pentaho.platform.api.repository2.unified.RepositoryFilePermission;
import org.pentaho.platform.api.repository2.unified.RepositoryFileSid;
import org.pentaho.platform.repository2.messages.Messages;
import org.pentaho.platform.repository2.unified.jcr.IAclMetadataStrategy.AclMetadata;
import org.pentaho.platform.repository2.unified.jcr.JcrRepositoryFileAclDao.IPermissionConversionHelper;
import org.pentaho.platform.repository2.unified.jcr.jackrabbit.security.SpringSecurityRolePrincipal;
import org.pentaho.platform.repository2.unified.jcr.jackrabbit.security.SpringSecurityUserPrincipal;

import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.AccessControlPolicyIterator;
import javax.jcr.security.Privilege;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * ACL utilities.
 * 
 * <p>
 * These utility methods are static because they are used from within Jackrabbit.
 * </p>
 * 
 * @author mlowery
 */
public class JcrRepositoryFileAclUtils {

  // ~ Static fields/initializers
  // ======================================================================================

  private static final Log logger = LogFactory.getLog( JcrRepositoryFileAclUtils.class );

  public static final String DEFAULT = "DEFAULT"; //$NON-NLS-1$

  public static final String SYSTEM_PROPERTY = "pentaho.repository.server.aclMetadataStrategy"; //$NON-NLS-1$

  private static String strategyName = System.getProperty( SYSTEM_PROPERTY );

  private static IAclMetadataStrategy strategy;

  static {
    initialize();
  }

  // ~ Instance fields
  // =================================================================================================

  // ~ Constructors
  // ====================================================================================================

  private JcrRepositoryFileAclUtils() {
    super();
  }

  // ~ Methods
  // =========================================================================================================

  private static void initialize() {
    if ( ( strategyName == null ) || "".equals( strategyName ) ) { //$NON-NLS-1$
      strategyName = DEFAULT;
    }

    if ( strategyName.equals( DEFAULT ) ) {
      strategy = new JcrAclMetadataStrategy();
    } else {
      // Try to load a custom strategy
      try {
        Class<?> clazz = Class.forName( strategyName );
        Constructor<?> customStrategy = clazz.getConstructor( new Class[] {} );
        strategy = (IAclMetadataStrategy) customStrategy.newInstance( new Object[] {} );
      } catch ( Exception e ) {
        throw new RuntimeException( e );
      }
    }

    logger.debug( "JcrRepositoryFileAclUtils initialized: strategy=" + strategyName ); //$NON-NLS-1$
  }

  public static AclMetadata getAclMetadata( final Session session, final String path, final AccessControlList acList )
    throws RepositoryException {
    return strategy.getAclMetadata( session, path, acList );
  }

  public static void setAclMetadata( final Session session, final String path, final AccessControlList acList,
      final AclMetadata aclMetadata ) throws RepositoryException {
    strategy.setAclMetadata( session, path, acList, aclMetadata );
  }

  public static List<AccessControlEntry> removeAclMetadata( final List<AccessControlEntry> acEntries )
    throws RepositoryException {
    return strategy.removeAclMetadata( acEntries );
  }

  /**
   * Expands all aggregate privileges.
   * 
   * @param privileges
   *          input privileges
   * @param expandNonStandardOnly
   *          if {@code true} expand only privileges outside of jcr: namespace
   * @return expanded privileges
   */
  public static Privilege[] expandPrivileges( final Privilege[] privileges, final boolean expandNonStandardOnly ) {
    // find all aggregate privileges and expand
    Set<Privilege> expandedPrivileges = new HashSet<Privilege>( Arrays.asList( privileges ) );
    while ( true ) {
      boolean foundAggregatePrivilege = false;
      List<Privilege> iterable = new ArrayList<Privilege>( expandedPrivileges );
      for ( Privilege privilege : iterable ) {
        // expand impl custom privileges (e.g. rep:write) but keep aggregates like jcr:write intact
        if ( !expandNonStandardOnly || !privilege.getName().startsWith( "jcr:" ) ) { //$NON-NLS-1$
          if ( privilege.isAggregate() ) {
            expandedPrivileges.remove( privilege );
            expandedPrivileges.addAll( Arrays.asList( privilege.getAggregatePrivileges() ) );
            foundAggregatePrivilege = true;
          }
        }
      }
      if ( !foundAggregatePrivilege ) {
        break;
      }
    }
    return expandedPrivileges.toArray( new Privilege[0] );
  }

  public static RepositoryFileAcl createAcl( Session session, PentahoJcrConstants pentahoJcrConstants,
      Serializable fileId, RepositoryFileAcl acl ) throws ItemNotFoundException, RepositoryException {
    Node node = session.getNodeByIdentifier( fileId.toString() );
    String absPath = node.getPath();
    AccessControlManager acMgr = session.getAccessControlManager();
    AccessControlList acList = getAccessControlList( acMgr, absPath );
    acMgr.setPolicy( absPath, acList );
    return internalUpdateAcl( session, pentahoJcrConstants, fileId, acl );
  }

  public static void
  addPermission( final Session session, final PentahoJcrConstants pentahoJcrConstants,
                   final Serializable fileId,
      final RepositoryFileSid recipient, final EnumSet<RepositoryFilePermission> permissions )
    throws RepositoryException {
    addAce( session, pentahoJcrConstants, fileId, recipient, permissions );
  }

  public static void setOwner( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final RepositoryFile file, final RepositoryFileSid owner ) throws RepositoryException {
    RepositoryFileSid newOwnerSid = owner;
    if ( JcrTenantUtils.getUserNameUtils().getTenant( owner.getName() ) == null ) {
      newOwnerSid = new RepositoryFileSid( JcrTenantUtils.getTenantedUser( owner.getName() ), owner.getType() );
    }
    RepositoryFileAcl acl = getAcl( session, pentahoJcrConstants, file.getId() );
    RepositoryFileAcl newAcl = new RepositoryFileAcl.Builder( acl ).owner( newOwnerSid ).build();
    updateAcl( session, newAcl );
  }

  public static void setFullControl( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final Serializable fileId, final RepositoryFileSid sid ) throws RepositoryException {
    addAce( session, pentahoJcrConstants, fileId, sid, EnumSet.of( RepositoryFilePermission.ALL ) );
  }

  public static void addAce( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final Serializable id, final RepositoryFileSid recipient, final EnumSet<RepositoryFilePermission> permission )
    throws RepositoryException {
    RepositoryFileSid newRecipient = recipient;
    if ( JcrTenantUtils.getUserNameUtils().getTenant( recipient.getName() ) == null ) {
      newRecipient = new RepositoryFileSid( JcrTenantUtils.getTenantedUser( recipient.getName() ),
        recipient.getType() );
    }
    RepositoryFileAcl acl = getAcl( session, pentahoJcrConstants, id );
    RepositoryFileAcl updatedAcl = new RepositoryFileAcl.Builder( acl ).ace( newRecipient, permission ).build();
    updateAcl( session, updatedAcl );
  }

  private static RepositoryFileAcl internalUpdateAcl( final Session session,
      final PentahoJcrConstants pentahoJcrConstants, final Serializable fileId, final RepositoryFileAcl acl )
    throws RepositoryException {
    Node node = session.getNodeByIdentifier( fileId.toString() );
    if ( node == null ) {
      throw new RepositoryException( "Node not found" ); //$NON-NLS-1$
    }
    String absPath = node.getPath();
    AccessControlManager acMgr = session.getAccessControlManager();
    AccessControlList acList = getAccessControlList( acMgr, absPath );

    // clear all entries
    AccessControlEntry[] acEntries = acList.getAccessControlEntries();
    for ( int i = 0; i < acEntries.length; i++ ) {
      acList.removeAccessControlEntry( acEntries[i] );
    }

    JcrRepositoryFileAclUtils.setAclMetadata( session, absPath, acList, new AclMetadata( acl.getOwner().getName(), acl
        .isEntriesInheriting() ) );

    // add entries to now empty list but only if not inheriting; force user to start with clean slate
    if ( !acl.isEntriesInheriting() ) {
      for ( RepositoryFileAce ace : acl.getAces() ) {
        Principal principal = null;
        if ( RepositoryFileSid.Type.ROLE == ace.getSid().getType() ) {
          principal = new SpringSecurityRolePrincipal( JcrTenantUtils.getTenantedRole( ace.getSid().getName() ) );
        } else {
          principal = new SpringSecurityUserPrincipal( JcrTenantUtils.getTenantedUser( ace.getSid().getName() ) );
        }
        IPermissionConversionHelper permissionConversionHelper = new DefaultPermissionConversionHelper( session );
        acList.addAccessControlEntry( principal, permissionConversionHelper.pentahoPermissionsToPrivileges( session,
            ace.getPermissions() ) );
      }
    }
    acMgr.setPolicy( absPath, acList );
    session.save();
    return getAcl( session, pentahoJcrConstants, fileId );
  }

  public static void updateAcl( final Session session, final RepositoryFileAcl acl ) throws RepositoryException {
    PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session );
    JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, acl.getId() );
    internalUpdateAcl( session, pentahoJcrConstants, acl.getId(), acl );
    JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, acl.getId(), null,
        null, true );
  }

  public static RepositoryFileAcl getAcl( final Session session, final PentahoJcrConstants pentahoJcrConstants,
      final Serializable id ) throws RepositoryException {

    Node node = session.getNodeByIdentifier( id.toString() );
    if ( node == null ) {
      throw new RepositoryException( Messages.getInstance().getString(
          "JackrabbitRepositoryFileAclDao.ERROR_0001_NODE_NOT_FOUND", id.toString() ) ); //$NON-NLS-1$
    }
    String absPath = node.getPath();
    AccessControlManager acMgr = session.getAccessControlManager();
    AccessControlList acList = getAccessControlList( acMgr, absPath );

    RepositoryFileSid owner = null;
    String ownerString = JcrTenantUtils.getUserNameUtils().getPrincipleName( getOwner( session, absPath, acList ) );

    if ( ownerString != null ) {
      // for now, just assume all owners are users; only has UI impact
      owner = new RepositoryFileSid( ownerString, RepositoryFileSid.Type.USER );
    }

    RepositoryFileAcl.Builder aclBuilder = new RepositoryFileAcl.Builder( id, owner );

    aclBuilder.entriesInheriting( isEntriesInheriting( session, absPath, acList ) );

    List<AccessControlEntry> cleanedAcEntries =
        JcrRepositoryFileAclUtils.removeAclMetadata( Arrays.asList( acList.getAccessControlEntries() ) );

    for ( AccessControlEntry acEntry : cleanedAcEntries ) {
      aclBuilder.ace( toAce( session, acEntry ) );
    }
    return aclBuilder.build();

  }

  private static AccessControlList getAccessControlList( final AccessControlManager acMgr, final String path )
    throws RepositoryException {
    AccessControlPolicyIterator applicablePolicies = acMgr.getApplicablePolicies( path );
    while ( applicablePolicies.hasNext() ) {
      AccessControlPolicy policy = applicablePolicies.nextAccessControlPolicy();
      if ( policy instanceof AccessControlList ) {
        return (AccessControlList) policy;
      }
    }
    AccessControlPolicy[] policies = acMgr.getPolicies( path );
    for ( int i = 0; i < policies.length; i++ ) {
      if ( policies[i] instanceof AccessControlList ) {
        return (AccessControlList) policies[i];
      }
    }
    throw new IllegalStateException( "no access control list applies or is bound to node" );
  }

  private static String getOwner( final Session session, final String path, final AccessControlList acList )
    throws RepositoryException {
    AclMetadata aclMetadata = JcrRepositoryFileAclUtils.getAclMetadata( session, path, acList );
    if ( aclMetadata != null ) {
      return aclMetadata.getOwner();
    } else {
      return null;
    }
  }

  private static boolean isEntriesInheriting( final Session session, final String path, final AccessControlList acList )
    throws RepositoryException {
    AclMetadata aclMetadata = JcrRepositoryFileAclUtils.getAclMetadata( session, path, acList );
    if ( aclMetadata != null ) {
      return aclMetadata.isEntriesInheriting();
    } else {
      return false;
    }
  }

  private static RepositoryFileAce toAce( final Session session, final AccessControlEntry acEntry )
    throws RepositoryException {
    Principal principal = acEntry.getPrincipal();
    RepositoryFileSid sid = null;
    if ( principal instanceof Group ) {
      sid = new RepositoryFileSid( principal.getName(), RepositoryFileSid.Type.ROLE );
    } else {
      sid = new RepositoryFileSid( principal.getName(), RepositoryFileSid.Type.USER );
    }
    Privilege[] privileges = acEntry.getPrivileges();
    IPermissionConversionHelper permissionConversionHelper = new DefaultPermissionConversionHelper( session );
    return new RepositoryFileAce( sid, permissionConversionHelper.privilegesToPentahoPermissions( session,
      privileges ) );
  }

}
